package ai.neuralnet.gen;

import ai.genetic.Individual;
import ai.neuralnet.NNBuilder;
import ai.neuralnet.NeuralNetwork;
import ai.neuralnet.Neuron;

import java.util.Arrays;
import java.util.List;
import java.util.Random;


public class GeneticNeuralNetwork extends NeuralNetwork<Neuron> implements Individual<GeneticNeuralNetwork>
{

    /**
     * random weight generation range
     */
    private double minGen = -1, maxGen = 1;

    /**
     * Various mutation operators
     */
    public static enum MutationStyle
    {
        Shift,
        Regen,
        BiasedRegen,
        Node,
    }

    /**
     * Various crossover operators
     */
    public static enum CrossoverStyle
    {
        Range,
        PickWeight,
        PickNeuron
    }

    /**
     * mutation style default is MutationStyle.Node
     */
    private MutationStyle mutationStyle = MutationStyle.Node;

    /**
     * crossover style default is CrossoverStyle.PickWeight
     */
    private CrossoverStyle crossoverStyle = CrossoverStyle.PickWeight;


    private Random rand = new Random();


    public GeneticNeuralNetwork(NNBuilder builder)
    {
        super(builder);

    }

    @Override
    public GeneticNeuralNetwork getObject()
    {
        return this;
    }

    public void crossover(List<Neuron> myNeurons, List<Neuron> pNeurons, List<Neuron> cNeurons)
    {
        for (int i = 0; i < myNeurons.size(); i++)
        {
            Neuron cNeuron = cNeurons.get(i);
            Neuron myNeuron = myNeurons.get(i);
            Neuron pNeuron = pNeurons.get(i);

            switch (crossoverStyle)
            {
                case Range:
                    crossRange(myNeuron, pNeuron, cNeuron);
                    break;
                case PickWeight:
                    crossPick(myNeuron, pNeuron, cNeuron);
                    break;
                case PickNeuron:
                    copyWeights(rand.nextBoolean() ? myNeuron : pNeuron, cNeuron);
                    break;
            }

        }
    }

    /**
     * performs crossover operation with the selected partner according to the selected style
     *
     * @param partner
     * @return
     */
    @Override
    public Individual<GeneticNeuralNetwork> crossover(Individual<GeneticNeuralNetwork> partner)
    {
        //gets underlying neural object from partner
        GeneticNeuralNetwork pNet = partner.getObject();
        // clones current network to make child
        GeneticNeuralNetwork child = clone();

        //overrides child weights based on selected crossover style
        crossover(hiddenNeurons, pNet.hiddenNeurons, child.hiddenNeurons);
        crossover(Arrays.asList(outputNeurons), Arrays.asList(pNet.outputNeurons), Arrays.asList(child.outputNeurons));

        return child;
    }

    private void crossRange(Neuron n1, Neuron n2, Neuron out)
    {
        for (int j = 0; j < out.getWeights().length; j++)
        {
            double min = Math.min(n1.getWeights()[j], n2.getWeights()[j]);
            double max = Math.max(n1.getWeights()[j], n2.getWeights()[j]);
            double fact = rand.nextBoolean() ? 0.1 : -0.1;
            out.getWeights()[j] = min + (max - min) * fact + rand.nextDouble() * (max - min);
        }
    }

    private void crossPick(Neuron n1, Neuron n2, Neuron out)
    {
        for (int j = 0; j < out.getWeights().length; j++)
        {
            out.getWeights()[j] = rand.nextBoolean() ? n1.getWeights()[j] : n2.getWeights()[j];
        }
    }


    private void copyWeights(Neuron n, Neuron out)
    {
        for (int j = 0; j < out.getWeights().length; j++)
        {
            out.getWeights()[j] = n.getWeights()[j];
        }
    }


    private void mutate(Iterable<Neuron> neurons)
    {
        for (Neuron n : neurons)
        {
            switch (mutationStyle)
            {
                case Shift:
                    mutateShift(n);
                    break;
                case Regen:
                    mutateRegen(n, false);
                    break;
                case BiasedRegen:
                    mutateRegen(n, true);
                    break;
                case Node:
                    mutateNode(n);
                    break;
            }

        }
    }

    @Override
    public void mutate()
    {
        mutate(hiddenNeurons);
        mutate(Arrays.asList(outputNeurons));
    }

    private void mutateShift(Neuron n)
    {

        for (int i = 0; i < n.getWeights().length; i++)
        {
            //mutate weight with 10% probability
            if (rand.nextDouble() > 0.1) continue;


            double factor = random(0.75, 1.25);

            n.getWeights()[i] *= factor;
        }
    }


    private double getRangeFactor()
    {
        return Math.pow(1 - getAccScore(), 1.1);
    }

    private void mutateNode(Neuron n)
    {
        //mutate nodes with 5% probability
        if (rand.nextDouble() > 0.05) return;

        double rf = getRangeFactor();

        double[] w = n.getWeights();

        for (int i = 0; i < w.length; i++)
        {
            w[i] += random(maxGen * rf, minGen * rf);
        }

    }

    private double random(double min, double max)
    {
        return min + (max - min) * rand.nextDouble();
    }

    private void mutateRegen(Neuron n, boolean bias)
    {
        double rf = getRangeFactor();

        for (int i = 0; i < n.getWeights().length; i++)
        {
            //mutate weights with probability 0.1
            if (rand.nextDouble() > 0.1) continue;

            if (bias)
            {
                n.getWeights()[i] += random(minGen * rf, maxGen * rf);
            }
            else
            {
                n.getWeights()[i] = random(minGen * rf, maxGen * rf);
            }
        }
    }

    public void setMinGen(double minGen)
    {
        this.minGen = minGen;
    }

    public void setMaxGen(double maxGen)
    {
        this.maxGen = maxGen;
    }

    @Override
    public GeneticNeuralNetwork clone()
    {
        GeneticNeuralNetwork copy = (GeneticNeuralNetwork) super.clone();
        copy.setMinGen(minGen);
        copy.setMaxGen(maxGen);
        copy.setMutationStyle(mutationStyle);
        copy.setCrossoverStyle(crossoverStyle);
        return copy;
    }


    public void setMutationStyle(MutationStyle mutationStyle)
    {
        this.mutationStyle = mutationStyle;
    }

    public void setCrossoverStyle(CrossoverStyle crossoverStyle)
    {
        this.crossoverStyle = crossoverStyle;
    }


}
